odhcpd: improve RFC9096 § 3.5 SLAAC compliance
authorÁlvaro Fernández Rojas <[email protected]>
Thu, 7 Aug 2025 15:38:22 +0000 (17:38 +0200)
committerÁlvaro Fernández Rojas <[email protected]>
Wed, 1 Oct 2025 16:25:56 +0000 (18:25 +0200)
When a CE router provides LAN-side address-configuration information
via SLAAC:

*  A CE router sending RAs that advertise prefixes belonging to a
   dynamically learned prefix (e.g., via DHCPv6-PD) SHOULD record, on
   stable storage, the list of prefixes being advertised via PIOs on
   each network segment and the state of the "A" and "L" flags of the
   corresponding PIOs.

*  Upon changes to the advertised prefixes, and after bootstrapping,
   the CE router advertising prefix information via SLAAC proceeds as
   follows:

   -  Any prefixes that were previously advertised by the CE router
      via PIOs in RA messages, but that have now become stale, MUST
      be advertised with PIOs that have the "Valid Lifetime" and the
      "Preferred Lifetime" set to 0 and the "A" and "L" bits
      unchanged.

   -  The aforementioned advertisements MUST be performed for at
      least the "Valid Lifetime" previously employed for such
      prefixes.

This should be enabled by default with a folder in "/tmp" to avoid forcing
flash writes for all the users and at least handle the stale PIOs on PPPoe
reconnections. For example:
  uci set dhcp.odhcpd.piofolder="/tmp/odhcpd-piofolder"
  uci commit dhcp

There's a new ubus call which allows getting all the IPv6 RA PIOs, with
information about their lifetime and stale status:
  ubus call dhcp ipv6ra
  {
    "interfaces": {
      "br-lan": [
        {
          "lifetime": 5390,
          "prefix": "2001:db8:0:100::/64",
          "stale": true
        },
        {
          "prefix": "2001:db8:0:200::/64",
          "stale": false
        },
        {
          "prefix": "fd00::/64",
          "stale": false
        }
      ],
      "eth1.10": [
        {
          "lifetime": 5390,
          "prefix": "2001:db8:0:110::/64",
          "stale": true
        },
        {
          "prefix": "2001:db8:0:210::/64",
          "stale": false
        },
        {
          "prefix": "fd00:0:0:10::/64",
          "stale": false
        }
      ]
    }
  }

The current implementation performs a flash write per interface whenever
a new prefix is added or an existing prefix becomes stale.
Since some ISPs assign static or semi-static prefixes to their customers,
it should be up to the user to enable flash writes when needed.

Link: https://github.com/openwrt/odhcpd/pull/256
Signed-off-by: Álvaro Fernández Rojas <[email protected]>
CMakeLists.txt
README
src/config.c
src/netlink.c
src/odhcpd.h
src/router.c
src/ubus.c

index de9a4e68ee578f1b663e81f2f362b99b54e99217..5690af34ab8662feba858d5e1d382c873bd12413 100644 (file)
@@ -11,6 +11,7 @@ FIND_PATH(ubox_include_dir libubox/uloop.h)
 FIND_PATH(libnl-tiny_include_dir netlink-generic.h PATH_SUFFIXES libnl-tiny)
 INCLUDE_DIRECTORIES(${ubox_include_dir} ${libnl-tiny_include_dir})
 
+FIND_LIBRARY(json NAMES json-c)
 FIND_LIBRARY(libnl NAMES nl-tiny)
 
 add_definitions(-D_GNU_SOURCE -Os -Wall -Werror --std=gnu99)
@@ -37,7 +38,7 @@ if(${DHCPV4_SUPPORT})
 endif(${DHCPV4_SUPPORT})
 
 add_executable(odhcpd src/odhcpd.c src/config.c src/router.c src/dhcpv6.c src/ndp.c src/dhcpv6-ia.c src/dhcpv6-pxe.c src/netlink.c ${EXT_SRC})
-target_link_libraries(odhcpd resolv ubox uci ${libnl} ${EXT_LINK})
+target_link_libraries(odhcpd resolv ubox uci ${json} ${libnl} ${EXT_LINK})
 
 # Installation
 install(TARGETS odhcpd DESTINATION sbin/)
diff --git a/README b/README
index bb9c0827f5a533fdadc246532138e541ef4b981a..582d8baf0c5a49c0ebd104f8d9b59553a91e6ea9 100644 (file)
--- a/README
+++ b/README
@@ -67,6 +67,8 @@ leasefile     string                          DHCP/v6 lease/hostfile
 leasetrigger   string                          Lease trigger script
 hostsfile      string                          DHCP/v6 hostfile
 loglevel       integer 6                       Syslog level priority (0-7)
+piofolder      string                          Folder to store IPv6 prefix information (to
+                                               detect stale prefixes, see RFC9096, §3.5)
 
 
 Sections of type dhcp (configure DHCP / DHCPv6 / RA / NDP service)
index a6b699ec41ea84fc32ae1dff747fc7d0d2ff00f6..72d3bde343d0feb4f3b0b9af594cc562c4531398 100644 (file)
@@ -1,3 +1,4 @@
+#include <errno.h>
 #include <fcntl.h>
 #include <resolv.h>
 #include <signal.h>
@@ -11,6 +12,7 @@
 
 #include <uci.h>
 #include <uci_blob.h>
+#include <json-c/json.h>
 #include <libubox/utils.h>
 #include <libubox/avl.h>
 #include <libubox/avl-cmp.h>
@@ -29,9 +31,16 @@ static void lease_update(struct vlist_tree *tree, struct vlist_node *node_new,
 
 struct vlist_tree leases = VLIST_TREE_INIT(leases, lease_cmp, lease_update, true, false);
 AVL_TREE(interfaces, avl_strcmp, false, NULL);
-struct config config = {.legacy = false, .main_dhcpv4 = false,
-                       .dhcp_cb = NULL, .dhcp_statefile = NULL, .dhcp_hostsfile = NULL,
-                       .log_level = LOG_WARNING};
+struct config config = {
+       .legacy = false,
+       .main_dhcpv4 = false,
+       .dhcp_cb = NULL,
+       .dhcp_statefile = NULL,
+       .dhcp_hostsfile = NULL,
+       .ra_piofolder = NULL,
+       .ra_piofolder_fd = -1,
+       .log_level = LOG_WARNING,
+};
 
 #define START_DEFAULT  100
 #define LIMIT_DEFAULT  150
@@ -200,6 +209,7 @@ enum {
        ODHCPD_ATTR_LEASETRIGGER,
        ODHCPD_ATTR_LOGLEVEL,
        ODHCPD_ATTR_HOSTSFILE,
+       ODHCPD_ATTR_PIOFOLDER,
        ODHCPD_ATTR_MAX
 };
 
@@ -210,6 +220,7 @@ static const struct blobmsg_policy odhcpd_attrs[ODHCPD_ATTR_MAX] = {
        [ODHCPD_ATTR_LEASETRIGGER] = { .name = "leasetrigger", .type = BLOBMSG_TYPE_STRING },
        [ODHCPD_ATTR_LOGLEVEL] = { .name = "loglevel", .type = BLOBMSG_TYPE_INT32 },
        [ODHCPD_ATTR_HOSTSFILE] = { .name = "hostsfile", .type = BLOBMSG_TYPE_STRING },
+       [ODHCPD_ATTR_PIOFOLDER] = { .name = "piofolder", .type = BLOBMSG_TYPE_STRING },
 };
 
 const struct uci_blob_param_list odhcpd_attr_list = {
@@ -276,6 +287,7 @@ static void set_interface_defaults(struct interface *iface)
        iface->ra_mininterval = iface->ra_maxinterval/3;
        iface->ra_lifetime = -1;
        iface->ra_dns = true;
+       iface->pio_update = false;
 }
 
 static void clean_interface(struct interface *iface)
@@ -318,7 +330,7 @@ static void close_interface(struct interface *iface)
        clean_interface(iface);
        free(iface->addr4);
        free(iface->addr6);
-       free(iface->invalid_addr6);
+       free(iface->pios);
        free(iface->ifname);
        free(iface);
 }
@@ -389,6 +401,11 @@ static void set_config(struct uci_section *s)
                config.dhcp_hostsfile = strdup(blobmsg_get_string(c));
        }
 
+       if ((c = tb[ODHCPD_ATTR_PIOFOLDER])) {
+               free(config.ra_piofolder);
+               config.ra_piofolder = strdup(blobmsg_get_string(c));
+       }
+
        if ((c = tb[ODHCPD_ATTR_LEASETRIGGER])) {
                free(config.dhcp_cb);
                config.dhcp_cb = strdup(blobmsg_get_string(c));
@@ -1480,6 +1497,8 @@ int config_parse_interface(void *data, size_t len, const char *name, bool overwr
                }
        }
 
+       config_load_ra_pio(iface);
+
        return 0;
 
 err:
@@ -1703,6 +1722,309 @@ static int ipv6_pxe_from_uci(struct uci_section* s)
        return ipv6_pxe_entry_new(arch, url) ? -1 : 0;
 }
 
+#define JSON_LENGTH "length"
+#define JSON_PREFIX "prefix"
+#define JSON_SLAAC "slaac"
+#define JSON_TIME "time"
+
+static inline time_t config_time_from_json(time_t json_time)
+{
+       time_t ref, now;
+
+       ref = time(NULL);
+       now = odhcpd_time();
+
+       if (now > json_time || ref > json_time)
+               return 0;
+
+       return json_time + (now - ref);
+}
+
+static inline time_t config_time_to_json(time_t config_time)
+{
+       time_t ref, now;
+
+       ref = time(NULL);
+       now = odhcpd_time();
+
+       return config_time + (ref - now);
+}
+
+static inline bool config_ra_pio_enabled(struct interface *iface)
+{
+       return config.ra_piofolder_fd >= 0 && iface->ra == MODE_SERVER && !iface->master;
+}
+
+static bool config_ra_pio_time(json_object *slaac_json, time_t *slaac_time)
+{
+       time_t pio_json_time, pio_time;
+       json_object *time_json;
+
+       time_json = json_object_object_get(slaac_json, JSON_TIME);
+       if (!time_json)
+               return true;
+
+       pio_json_time = (time_t) json_object_get_int64(time_json);
+       if (!pio_json_time)
+               return true;
+
+       pio_time = config_time_from_json(pio_json_time);
+       if (!pio_time)
+               return false;
+
+       *slaac_time = pio_time;
+
+       return true;
+}
+
+static json_object *config_load_ra_pio_json(struct interface *iface)
+{
+       json_object *json;
+       int fd;
+
+       fd = openat(config.ra_piofolder_fd, iface->ifname, O_RDONLY | O_CLOEXEC);
+       if (fd < 0)
+               return NULL;
+
+       json = json_object_from_fd(fd);
+
+       close(fd);
+
+       if (!json)
+               syslog(LOG_ERR,
+                       "rfc9096: %s: json read error %s",
+                       iface->ifname,
+                       json_util_get_last_err());
+
+       return json;
+}
+
+void config_load_ra_pio(struct interface *iface)
+{
+       json_object *json, *slaac_json;
+       size_t pio_cnt;
+       time_t now;
+
+       if (!config_ra_pio_enabled(iface))
+               return;
+
+       json = config_load_ra_pio_json(iface);
+       if (!json)
+               return;
+
+       slaac_json = json_object_object_get(json, JSON_SLAAC);
+       if (!slaac_json) {
+               json_object_put(json);
+               return;
+       }
+
+       now = odhcpd_time();
+
+       pio_cnt = json_object_array_length(slaac_json);
+       iface->pios = malloc(sizeof(struct ra_pio) * pio_cnt);
+       if (!iface->pios) {
+               json_object_put(json);
+               return;
+       }
+
+       iface->pio_cnt = 0;
+       for (size_t i = 0; i < pio_cnt; i++) {
+               json_object *cur_pio_json, *length_json, *prefix_json;
+               const char *pio_str;
+               time_t pio_lt = 0;
+               struct ra_pio *pio;
+               uint8_t pio_len;
+
+               cur_pio_json = json_object_array_get_idx(slaac_json, i);
+               if (!cur_pio_json)
+                       continue;
+
+               if (!config_ra_pio_time(cur_pio_json, &pio_lt))
+                       continue;
+
+               length_json = json_object_object_get(cur_pio_json, JSON_LENGTH);
+               if (!length_json)
+                       continue;
+
+               prefix_json = json_object_object_get(cur_pio_json, JSON_PREFIX);
+               if (!prefix_json)
+                       continue;
+
+               pio_len = (uint8_t) json_object_get_uint64(length_json);
+               pio_str = json_object_get_string(prefix_json);
+               pio = &iface->pios[iface->pio_cnt];
+
+               inet_pton(AF_INET6, pio_str, &pio->prefix);
+               pio->length = pio_len;
+               pio->lifetime = pio_lt;
+               syslog(LOG_INFO,
+                       "rfc9096: %s: load %s/%u (%u)",
+                       iface->ifname,
+                       pio_str,
+                       pio_len,
+                       ra_pio_lifetime(pio, now));
+
+               iface->pio_cnt++;
+       }
+
+       json_object_put(json);
+
+       if (!iface->pio_cnt) {
+               free(iface->pios);
+               iface->pios = NULL;
+       } else if (iface->pio_cnt != pio_cnt) {
+               iface->pios = realloc(iface->pios, sizeof(struct ra_pio) * iface->pio_cnt);
+       }
+}
+
+static void config_save_ra_pio_json(struct interface *iface, struct json_object *json)
+{
+       size_t tmp_piofile_strlen;
+       char *tmp_piofile;
+       int fd, ret;
+
+       tmp_piofile_strlen = strlen(iface->ifname) + 2;
+       tmp_piofile = alloca(tmp_piofile_strlen);
+       snprintf(tmp_piofile, tmp_piofile_strlen, ".%s", iface->ifname);
+
+       fd = openat(config.ra_piofolder_fd,
+               tmp_piofile,
+               O_CREAT | O_TRUNC | O_WRONLY | O_CLOEXEC,
+               0644);
+       if (fd < 0) {
+               syslog(LOG_ERR,
+                       "rfc9096: %s: error %m creating temporary json file",
+                       iface->ifname);
+               return;
+       }
+
+       ret = json_object_to_fd(fd, json, JSON_C_TO_STRING_PLAIN);
+       if (ret) {
+               syslog(LOG_ERR,
+                       "rfc9096: %s: json write error %s",
+                       iface->ifname,
+                       json_util_get_last_err());
+               close(fd);
+               unlinkat(config.ra_piofolder_fd, tmp_piofile, 0);
+               return;
+       }
+
+       ret = fsync(fd);
+       if (ret) {
+               syslog(LOG_ERR,
+                       "rfc9096: %s: error %m syncing %s",
+                       iface->ifname,
+                       tmp_piofile);
+               close(fd);
+               unlinkat(config.ra_piofolder_fd, tmp_piofile, 0);
+               return;
+       }
+
+       ret = close(fd);
+       if (ret) {
+               syslog(LOG_ERR,
+                       "rfc9096: %s: error %m closing %s",
+                       iface->ifname,
+                       tmp_piofile);
+               unlinkat(config.ra_piofolder_fd, tmp_piofile, 0);
+               return;
+       }
+
+       ret = renameat(config.ra_piofolder_fd,
+               tmp_piofile,
+               config.ra_piofolder_fd,
+               iface->ifname);
+       if (ret) {
+               syslog(LOG_ERR,
+                       "rfc9096: %s: error %m renaming piofile: %s -> %s",
+                       iface->ifname,
+                       tmp_piofile,
+                       iface->ifname);
+               close(fd);
+               unlinkat(config.ra_piofolder_fd, tmp_piofile, 0);
+               return;
+       }
+
+       iface->pio_update = false;
+       syslog(LOG_WARNING,
+               "rfc9096: %s: piofile updated",
+               iface->ifname);
+}
+
+void config_save_ra_pio(struct interface *iface)
+{
+       struct json_object *json, *slaac_json;
+       char ipv6_str[INET6_ADDRSTRLEN];
+       time_t now;
+
+       if (!config_ra_pio_enabled(iface))
+               return;
+
+       if (!iface->pio_update)
+               return;
+
+       now = odhcpd_time();
+
+       json = json_object_new_object();
+       if (!json)
+               return;
+
+       slaac_json = json_object_new_array_ext(iface->pio_cnt);
+       if (!slaac_json) {
+               json_object_put(slaac_json);
+               return;
+       }
+
+       json_object_object_add(json, JSON_SLAAC, slaac_json);
+
+       for (size_t i = 0; i < iface->pio_cnt; i++) {
+               struct json_object *cur_pio_json, *len_json, *pfx_json;
+               const struct ra_pio *cur_pio = &iface->pios[i];
+
+               if (ra_pio_expired(cur_pio, now))
+                       continue;
+
+               cur_pio_json = json_object_new_object();
+               if (!cur_pio_json)
+                       continue;
+
+               inet_ntop(AF_INET6, &cur_pio->prefix, ipv6_str, sizeof(ipv6_str));
+
+               pfx_json = json_object_new_string(ipv6_str);
+               if (!pfx_json) {
+                       json_object_put(cur_pio_json);
+                       continue;
+               }
+
+               len_json = json_object_new_uint64(cur_pio->length);
+               if (!len_json) {
+                       json_object_put(cur_pio_json);
+                       json_object_put(pfx_json);
+                       continue;
+               }
+
+               json_object_object_add(cur_pio_json, JSON_PREFIX, pfx_json);
+               json_object_object_add(cur_pio_json, JSON_LENGTH, len_json);
+
+               if (cur_pio->lifetime) {
+                       struct json_object *time_json;
+                       time_t pio_lt;
+
+                       pio_lt = config_time_to_json(cur_pio->lifetime);
+
+                       time_json = json_object_new_int64(pio_lt);
+                       if (time_json)
+                               json_object_object_add(cur_pio_json, JSON_TIME, time_json);
+               }
+
+               json_object_array_add(slaac_json, cur_pio_json);
+       }
+
+       config_save_ra_pio_json(iface, json);
+
+       json_object_put(json);
+}
+
 void odhcpd_reload(void)
 {
        struct uci_context *uci = uci_alloc_context();
@@ -1757,6 +2079,17 @@ void odhcpd_reload(void)
                free(path);
        }
 
+       if (config.ra_piofolder) {
+               char *path = strdupa(config.ra_piofolder);
+
+               mkdir_p(path, 0755);
+
+               close(config.ra_piofolder_fd);
+               config.ra_piofolder_fd = open(path, O_PATH | O_DIRECTORY | O_CLOEXEC);
+               if (config.ra_piofolder_fd < 0)
+                       syslog(LOG_ERR, "Unable to open piofolder '%s': %m", path);
+       }
+
        vlist_flush(&leases);
 
 #ifdef WITH_UBUS
index 6b38caab339048ff142f8233549a783abab53e9b..57cf56699b7e5744cfc83def3e1861b6160c16a9 100644 (file)
@@ -210,61 +210,6 @@ static void refresh_iface_addr6(int ifindex)
                                    addr[i].valid_lt < iface->addr6[i].valid_lt || addr[i].preferred_lt < iface->addr6[i].preferred_lt)
                                        change = true;
                        }
-
-                       if (change) {
-                               /*
-                                * Keep track of removed prefixes, so we could advertise them as invalid
-                                * for at least a couple of times.
-                                *
-                                * RFC7084 § 4.3 :
-                                *    L-13:  If the delegated prefix changes, i.e., the current prefix is
-                                *           replaced with a new prefix without any overlapping time
-                                *           period, then the IPv6 CE router MUST immediately advertise the
-                                *           old prefix with a Preferred Lifetime of zero and a Valid
-                                *           Lifetime of either a) zero or b) the lower of the current
-                                *           Valid Lifetime and two hours (which must be decremented in
-                                *           real time) in a Router Advertisement message as described in
-                                *           Section 5.5.3, (e) of [RFC4862].
-                                */
-
-                               for (size_t i = 0; i < iface->addr6_len; ++i) {
-                                       bool removed = true;
-
-                                       if (iface->addr6[i].valid_lt <= (uint32_t)now)
-                                               continue;
-
-                                       for (ssize_t j = 0; removed && j < len; ++j) {
-                                               size_t plen = min(addr[j].prefix, iface->addr6[i].prefix);
-
-                                               if (odhcpd_bmemcmp(&addr[j].addr.in6, &iface->addr6[i].addr.in6, plen) == 0)
-                                                       removed = false;
-                                       }
-
-                                       for (size_t j = 0; removed && j < iface->invalid_addr6_len; ++j) {
-                                               size_t plen = min(iface->invalid_addr6[j].prefix, iface->addr6[i].prefix);
-
-                                               if (odhcpd_bmemcmp(&iface->invalid_addr6[j].addr.in6, &iface->addr6[i].addr.in6, plen) == 0)
-                                                       removed = false;
-                                       }
-
-                                       if (removed) {
-                                               size_t pos = iface->invalid_addr6_len;
-                                               struct odhcpd_ipaddr *new_invalid_addr6 = realloc(iface->invalid_addr6,
-                                                               sizeof(*iface->invalid_addr6) * (pos + 1));
-
-                                               if (!new_invalid_addr6)
-                                                       break;
-
-                                               iface->invalid_addr6 = new_invalid_addr6;
-                                               iface->invalid_addr6_len++;
-                                               memcpy(&iface->invalid_addr6[pos], &iface->addr6[i], sizeof(*iface->invalid_addr6));
-                                               iface->invalid_addr6[pos].valid_lt = iface->invalid_addr6[pos].preferred_lt = (uint32_t)now;
-
-                                               if (iface->invalid_addr6[pos].prefix < 64)
-                                                       iface->invalid_addr6[pos].prefix = 64;
-                                       }
-                               }
-                       }
                }
 
                iface->addr6 = addr;
index 4c510e23c25c02adabe636b7b114f59a89293005..5b7b49f24ffe45ea5db3b6dc36c6e2ed1b579dca 100644 (file)
@@ -140,7 +140,6 @@ struct odhcpd_ipaddr {
                /* ipv6 only */
                struct {
                        uint8_t dprefix;
-                       uint8_t invalid_advertisements;
                        bool tentative;
                };
 
@@ -173,6 +172,10 @@ struct config {
        char *dhcp_cb;
        char *dhcp_statefile;
        char *dhcp_hostsfile;
+
+       char *ra_piofolder;
+       int ra_piofolder_fd;
+
        int log_level;
 };
 
@@ -270,6 +273,14 @@ struct dnr_options {
 };
 
 
+// RA PIO - RFC9096
+struct ra_pio {
+       struct in6_addr prefix;
+       uint8_t length;
+       time_t lifetime;
+};
+
+
 struct interface {
        struct avl_node avl;
 
@@ -281,8 +292,6 @@ struct interface {
        // IPv6 runtime data
        struct odhcpd_ipaddr *addr6;
        size_t addr6_len;
-       struct odhcpd_ipaddr *invalid_addr6;
-       size_t invalid_addr6_len;
 
        // RA runtime data
        struct odhcpd_event router_event;
@@ -403,6 +412,11 @@ struct interface {
        // DNR
        struct dnr_options *dnr;
        size_t dnr_cnt;
+
+       // RA PIO - RFC9096
+       struct ra_pio *pios;
+       size_t pio_cnt;
+       bool pio_update;
 };
 
 extern struct avl_tree interfaces;
@@ -434,6 +448,24 @@ inline static struct dhcp_assignment *alloc_assignment(size_t extra_len)
        return a;
 }
 
+inline static bool ra_pio_expired(const struct ra_pio *pio, time_t now)
+{
+       return pio->lifetime && (now > pio->lifetime);
+}
+
+inline static uint32_t ra_pio_lifetime(const struct ra_pio *pio, time_t now)
+{
+       if (!pio->lifetime || now > pio->lifetime)
+               return 0;
+
+       return (uint32_t) (pio->lifetime - now);
+}
+
+inline static bool ra_pio_stale(const struct ra_pio *pio)
+{
+       return !!pio->lifetime;
+}
+
 // Exported main functions
 int odhcpd_register(struct odhcpd_event *event);
 int odhcpd_deregister(struct odhcpd_event *event);
@@ -469,6 +501,8 @@ struct lease *config_find_lease_by_duid(const uint8_t *duid, const uint16_t len)
 struct lease *config_find_lease_by_mac(const uint8_t *mac);
 struct lease *config_find_lease_by_hostid(const uint64_t hostid);
 struct lease *config_find_lease_by_ipaddr(const uint32_t ipaddr);
+void config_load_ra_pio(struct interface *iface);
+void config_save_ra_pio(struct interface *iface);
 int set_lease_from_blobmsg(struct blob_attr *ba);
 
 #ifdef WITH_UBUS
index 7982ccffc7ee9afc42f64cdfb6e6188ba7378344..d8b69902a0f93c2d4917fc4e2e9acad03d8e0c72 100644 (file)
@@ -446,6 +446,116 @@ struct nd_opt_dnr_info {
        uint8_t body[];
 };
 
+/* IPv6 RA PIOs */
+static struct ra_pio *router_find_ra_pio(struct interface *iface,
+       struct nd_opt_prefix_info *p)
+{
+       for (size_t i = 0; i < iface->pio_cnt; i++) {
+               struct ra_pio *cur_pio = &iface->pios[i];
+
+               if (p->nd_opt_pi_prefix_len == cur_pio->length &&
+                       !odhcpd_bmemcmp(&p->nd_opt_pi_prefix, &cur_pio->prefix, cur_pio->length))
+                       return cur_pio;
+       }
+
+       return NULL;
+}
+
+static void router_add_ra_pio(struct interface *iface,
+       struct nd_opt_prefix_info *p)
+{
+       char ipv6_str[INET6_ADDRSTRLEN];
+       struct ra_pio *new_pios, *pio;
+
+       pio = router_find_ra_pio(iface, p);
+       if (pio) {
+               if (pio->lifetime) {
+                       pio->lifetime = 0;
+
+                       iface->pio_update = true;
+                       syslog(LOG_WARNING, "rfc9096: %s: renew %s/%u",
+                               iface->ifname,
+                               inet_ntop(AF_INET6, &pio->prefix, ipv6_str, sizeof(ipv6_str)),
+                               pio->length);
+               }
+
+               return;
+       }
+
+       new_pios = realloc(iface->pios, sizeof(struct ra_pio) * (iface->pio_cnt + 1));
+       if (!new_pios)
+        return;
+
+       iface->pios = new_pios;
+       pio = &iface->pios[iface->pio_cnt];
+       iface->pio_cnt++;
+
+       memcpy(&pio->prefix, &p->nd_opt_pi_prefix, sizeof(pio->prefix));
+       pio->length = p->nd_opt_pi_prefix_len;
+       pio->lifetime = 0;
+
+       iface->pio_update = true;
+       syslog(LOG_INFO, "rfc9096: %s: add %s/%u",
+               iface->ifname,
+               inet_ntop(AF_INET6, &pio->prefix, ipv6_str, sizeof(ipv6_str)),
+               pio->length);
+}
+
+static void router_clear_ra_pio(time_t now,
+       struct interface *iface)
+{
+       size_t i = 0, pio_cnt = iface->pio_cnt;
+       char ipv6_str[INET6_ADDRSTRLEN];
+
+       while (i < iface->pio_cnt) {
+               struct ra_pio *cur_pio = &iface->pios[i];
+
+               if (ra_pio_expired(cur_pio, now)) {
+                       syslog(LOG_INFO,
+                               "rfc9096: %s: clear %s/%u",
+                               iface->ifname,
+                               inet_ntop(AF_INET6, &cur_pio->prefix, ipv6_str, sizeof(ipv6_str)),
+                               cur_pio->length);
+
+                       if (i + 1 < iface->pio_cnt)
+                               iface->pios[i] = iface->pios[iface->pio_cnt - 1];
+
+                       iface->pio_cnt--;
+               } else {
+                       i++;
+               }
+       }
+
+       if (!iface->pio_cnt) {
+               free(iface->pios);
+               iface->pios = NULL;
+       } else if (iface->pio_cnt != pio_cnt) {
+               struct ra_pio *new_pios = realloc(iface->pios, sizeof(struct ra_pio) * iface->pio_cnt);
+
+               if (new_pios)
+                       iface->pios = new_pios;
+       }
+}
+
+static void router_stale_ra_pio(struct interface *iface,
+       struct nd_opt_prefix_info *p,
+       time_t now)
+{
+       struct ra_pio *pio = router_find_ra_pio(iface, p);
+       char ipv6_str[INET6_ADDRSTRLEN];
+
+       if (!pio || pio->lifetime)
+               return;
+
+       pio->lifetime = now + iface->max_valid_lifetime;
+
+       iface->pio_update = true;
+       syslog(LOG_WARNING, "rfc9096: %s: stale %s/%u",
+               iface->ifname,
+               inet_ntop(AF_INET6, &pio->prefix, ipv6_str, sizeof(ipv6_str)),
+               pio->length);
+}
+
 /* Router Advert server mode */
 static int send_router_advert(struct interface *iface, const struct in6_addr *from)
 {
@@ -463,7 +573,7 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr
        struct sockaddr_in6 dest;
        size_t dns_sz = 0, search_sz = 0, pref64_sz = 0, dnrs_sz = 0;
        size_t pfxs_cnt = 0, routes_cnt = 0;
-       size_t valid_addr_cnt = 0, invalid_addr_cnt = 0;
+       size_t total_addr_cnt = 0, valid_addr_cnt = 0;
        /* 
         * lowest_found_lifetime stores the lowest lifetime of all prefixes;
         * necessary to find longest adv interval necessary
@@ -475,6 +585,8 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr
        bool valid_prefix = false;
        char buf[INET6_ADDRSTRLEN];
 
+       router_clear_ra_pio(now, iface);
+
        memset(&adv, 0, sizeof(adv));
        adv.h.nd_ra_type = ND_ROUTER_ADVERT;
 
@@ -513,7 +625,6 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr
        iov[IOV_RA_ADV].iov_len = sizeof(adv);
 
        valid_addr_cnt = (iface->timer_rs.cb /* if not shutdown */ ? iface->addr6_len : 0);
-       invalid_addr_cnt = iface->invalid_addr6_len;
 
        // check ra_default
        if (iface->default_router) {
@@ -523,47 +634,46 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr
                        valid_prefix = true;
        }
 
-       if (valid_addr_cnt + invalid_addr_cnt) {
-               addrs = alloca(sizeof(*addrs) * (valid_addr_cnt + invalid_addr_cnt));
+       if (valid_addr_cnt + iface->pio_cnt) {
+               addrs = alloca(sizeof(*addrs) * (valid_addr_cnt + iface->pio_cnt));
 
                if (valid_addr_cnt) {
                        memcpy(addrs, iface->addr6, sizeof(*addrs) * valid_addr_cnt);
+                       total_addr_cnt = valid_addr_cnt;
 
                        /* Check default route */
                        if (!default_route && parse_routes(addrs, valid_addr_cnt))
                                default_route = true;
                }
 
-               if (invalid_addr_cnt) {
-                       size_t i = 0;
+               for (size_t i = 0; i < iface->pio_cnt; i++) {
+                       struct ra_pio *cur_pio = &iface->pios[i];
+                       bool pio_found = false;
 
-                       memcpy(&addrs[valid_addr_cnt], iface->invalid_addr6, sizeof(*addrs) * invalid_addr_cnt);
+                       for (size_t j = 0; j < valid_addr_cnt; j++) {
+                               struct odhcpd_ipaddr *cur_addr = &addrs[j];
 
-                       /* Remove invalid prefixes that were advertised 3 times */
-                       while (i < iface->invalid_addr6_len) {
-                               if (++iface->invalid_addr6[i].invalid_advertisements >= 3) {
-                                       if (i + 1 < iface->invalid_addr6_len)
-                                               memmove(&iface->invalid_addr6[i], &iface->invalid_addr6[i + 1], sizeof(*addrs) * (iface->invalid_addr6_len - i - 1));
-
-                                       iface->invalid_addr6_len--;
+                               if (cur_pio->length == cur_addr->prefix &&
+                                       !odhcpd_bmemcmp(&cur_pio->prefix, &cur_addr->addr.in6, cur_pio->length)) {
+                                       pio_found = true;
+                                       break;
+                               }
+                       }
 
-                                       if (iface->invalid_addr6_len) {
-                                               struct odhcpd_ipaddr *new_invalid_addr6 = realloc(iface->invalid_addr6, sizeof(*addrs) * iface->invalid_addr6_len);
+                       if (!pio_found) {
+                               struct odhcpd_ipaddr *addr = &addrs[total_addr_cnt];
 
-                                               if (new_invalid_addr6)
-                                                       iface->invalid_addr6 = new_invalid_addr6;
-                                       } else {
-                                               free(iface->invalid_addr6);
-                                               iface->invalid_addr6 = NULL;
-                                       }
-                               } else
-                                       ++i;
+                               memcpy(&addr->addr.in6, &cur_pio->prefix, sizeof(addr->addr.in6));
+                               addr->prefix = cur_pio->length;
+                               addr->preferred_lt = 0;
+                               addr->valid_lt = (uint32_t) (now + ND_VALID_LIMIT);
+                               total_addr_cnt++;
                        }
                }
        }
 
        /* Construct Prefix Information options */
-       for (size_t i = 0; i < valid_addr_cnt + invalid_addr_cnt; ++i) {
+       for (size_t i = 0; i < total_addr_cnt; ++i) {
                struct odhcpd_ipaddr *addr = &addrs[i];
                struct nd_opt_prefix_info *p = NULL;
                uint32_t preferred_lt = 0;
@@ -651,8 +761,26 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr
                        p->nd_opt_pi_flags_reserved |= ND_OPT_PI_FLAG_AUTO;
                if (iface->ra_advrouter)
                        p->nd_opt_pi_flags_reserved |= ND_OPT_PI_FLAG_RADDR;
-               p->nd_opt_pi_preferred_time = htonl(preferred_lt);
-               p->nd_opt_pi_valid_time = htonl(valid_lt);
+               if (i >= valid_addr_cnt || !preferred_lt) {
+                       /* 
+                        * RFC9096 § 3.5
+                        *
+                        * - Any prefixes that were previously advertised by the CE router
+                        *   via PIOs in RA messages, but that have now become stale, MUST
+                        *   be advertised with PIOs that have the "Valid Lifetime" and the
+                        *   "Preferred Lifetime" set to 0 and the "A" and "L" bits
+                        *   unchanged.
+                        */
+                       p->nd_opt_pi_preferred_time = 0;
+                       p->nd_opt_pi_valid_time = 0;
+
+                       router_stale_ra_pio(iface, p, now);
+               } else {
+                       p->nd_opt_pi_preferred_time = htonl(preferred_lt);
+                       p->nd_opt_pi_valid_time = htonl(valid_lt);
+
+                       router_add_ra_pio(iface, p);
+               }
        }
 
        iov[IOV_RA_PFXS].iov_base = (char *)pfxs;
@@ -892,9 +1020,12 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr
 
        syslog(LOG_NOTICE, "Sending a RA on %s", iface->name);
 
-       if (odhcpd_send(iface->router_event.uloop.fd, &dest, iov, ARRAY_SIZE(iov), iface) > 0)
+       if (odhcpd_send(iface->router_event.uloop.fd, &dest, iov, ARRAY_SIZE(iov), iface) > 0) {
                iface->ra_sent++;
 
+               config_save_ra_pio(iface);
+       }
+
 out:
        free(pfxs);
        free(routes);
index be3deac4d338f2d4c1fe5b15945b2b3eb6e7d12b..36d7dbb3229650e5e1717c28e978ce71ccecacd9 100644 (file)
@@ -176,6 +176,62 @@ static int handle_dhcpv6_leases(_unused struct ubus_context *ctx, _unused struct
        return 0;
 }
 
+static int handle_ra_pio(_unused struct ubus_context *ctx, _unused struct ubus_object *obj,
+               _unused struct ubus_request_data *req, _unused const char *method,
+               _unused struct blob_attr *msg)
+{
+       char ipv6_str[INET6_ADDRSTRLEN];
+       time_t now = odhcpd_time();
+       struct interface *iface;
+       void *interfaces_blob;
+
+       blob_buf_init(&b, 0);
+
+       interfaces_blob = blobmsg_open_table(&b, "interfaces");
+
+       avl_for_each_element(&interfaces, iface, avl) {
+               void *interface_blob;
+
+               if (iface->ra != MODE_SERVER)
+                       continue;
+
+               interface_blob = blobmsg_open_array(&b, iface->ifname);
+
+               for (size_t i = 0; i < iface->pio_cnt; i++) {
+                       struct ra_pio *cur_pio = &iface->pios[i];
+                       void *cur_pio_blob;
+                       uint32_t pio_lt;
+                       bool pio_stale;
+
+                       if (ra_pio_expired(cur_pio, now))
+                               continue;
+
+                       cur_pio_blob = blobmsg_open_table(&b, NULL);
+
+                       pio_lt = ra_pio_lifetime(cur_pio, now);
+                       pio_stale = ra_pio_stale(cur_pio);
+
+                       inet_ntop(AF_INET6, &cur_pio->prefix, ipv6_str, sizeof(ipv6_str));
+
+                       if (pio_lt)
+                               blobmsg_add_u32(&b, "lifetime", pio_lt);
+                       blobmsg_add_string(&b, "prefix", ipv6_str);
+                       blobmsg_add_u16(&b, "length", cur_pio->length);
+                       blobmsg_add_u8(&b, "stale", pio_stale);
+
+                       blobmsg_close_table(&b, cur_pio_blob);
+               }
+
+               blobmsg_close_array(&b, interface_blob);
+       }
+
+       blobmsg_close_table(&b, interfaces_blob);
+
+       ubus_send_reply(ctx, req, b.head);
+
+       return 0;
+}
+
 static int handle_add_lease(_unused struct ubus_context *ctx, _unused struct ubus_object *obj,
                _unused struct ubus_request_data *req, _unused const char *method,
                struct blob_attr *msg)
@@ -189,6 +245,7 @@ static int handle_add_lease(_unused struct ubus_context *ctx, _unused struct ubu
 static struct ubus_method main_object_methods[] = {
        {.name = "ipv4leases", .handler = handle_dhcpv4_leases},
        {.name = "ipv6leases", .handler = handle_dhcpv6_leases},
+       {.name = "ipv6ra", .handler = handle_ra_pio},
        UBUS_METHOD("add_lease", handle_add_lease, lease_attrs),
 };